Skip to content

feat(operator): introduce v0.1 local web UI - #39

Merged
magix022 merged 25 commits into
mainfrom
add-operator-web-ui-and-hot-reload
Aug 5, 2026
Merged

feat(operator): introduce v0.1 local web UI#39
magix022 merged 25 commits into
mainfrom
add-operator-web-ui-and-hot-reload

Conversation

@magix022

@magix022 magix022 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Rationale

The local operator exposed control and observability only through the Textual TUI. Browser consumers could not observe catalog reloads, and historical runs did not retain the topology required to render the workflow that actually executed. This v0.1 adds a local-first web interface while preserving the operator as the authoritative state owner and keeping current workflow definitions distinct from recorded run state.

Summary

  • Add the v0.1 React/TypeScript/Vite operator web application (web/operator) and package its built assets with the Python runtime. It uses generated gRPC-Web clients, React Flow, CodeMirror, virtualized lists, Tailwind styling, and component/state/API coverage.
  • Add an optional in-process gRPC-Web/static-asset listener. uv run ava operator --flows examples --web serves the interface on 127.0.0.1:7435 by default; non-loopback exposure requires explicit trusted-proxy configuration. The listener has no built-in authentication, so this remains a local-development surface.
  • Add Explorer navigation for scan targets, workflows, and runs; current-workflow blueprints with pan/zoom and typed declaration cards; and separate run canvases that show execution state, duration, failures, and retained topology rather than live catalog definitions.
  • Add run controls for default no-input execution, optional JSON-object input through a closed-by-default editor, authoritative validation errors, and cancellation.
  • Add browser inspection for declaration Markdown, agent inputs/outputs, tagged PredictRLM file paths, virtualized/paginated RunTrace data, bounded expandable JSON values, and a canonical collapsible, node-scoped or run-wide log pane with resize and explicit follow-latest behavior.
  • Preserve immutable, schema-only executed topology and agent declaration metadata with each run. Historical runs stay renderable when a workflow is changed or removed; reloads affect only the current catalog and future runs.
  • Make discovery reloads atomic and observable: validate candidate catalogs, retain the last valid catalog on failure, publish monotonic catalog revisions and reset/replacement updates, group scan-target metadata, and avoid updates for semantically unchanged sources.
  • Extend the operator protocol, Python client, TUI provider, gRPC-Web adapter, conversion layer, and generated protobuf/TypeScript bindings for catalog updates, snapshots, paginated logs/agent events, trace details, failure messages, and bounded hydration.
  • Bound browser and transport work for large runs with summary baselines, selected-run snapshot loading, cancellable single-page readers, bounded update queues/tails/caches, server-filtered paging, virtualized rendering, and contained graph/explorer rerenders.
  • Add operator lifecycle logging (--log-level) for watcher startup/shutdown and reload attempt, success, unchanged, and failure outcomes.
  • Update CLI, README, getting-started, agent-step documentation, changelog, Make targets, OpenSpec design/spec/tasks, and operator/TUI/agent regression coverage.

Test Plan

  • make lint
  • make web-test — 11 Vitest files / 85 tests passed.
  • make web-build — TypeScript checks and packaged Vite asset build passed.
  • uv run pytest test/cli_test.py test/agent/agent_step_test.py test/agent_rlm_logs_test.py test/operator_tests/test_grpc.py test/operator_tests/test_grpc_client_auth_tls.py test/operator_tests/test_operator.py test/operator_tests/test_operator_dev_reload.py test/operator_tests/test_protocol_contract.py test/operator_tests/test_registry.py test/operator_tests/test_run_updates.py test/operator_tests/test_state_detail.py test/operator_tests/test_web.py test/tui_test.py -q
    • Result: 543 passed, 15 failed, 11 warnings (Python 3.13; 15m28s).
    • test_operator_dev_reload.py: 11 failures covering live-source reload, preparation/build failure publication, malformed preparation events, cancellation/running ordering, preparation timeout, and close-during-preparation.
    • test_state_detail.py: 3 failures: initial run status is REQUESTING instead of the test's expected PENDING; a test fixture omits the newly accessed preparation_thread; and an expected large agent-event detail callback is absent.
    • test_web.py::test_browser_listener_serves_packaged_operator_application: the assertion expects the exact legacy <div id="root"></div> markup, while the built application includes classes on that root element.

Reviewer Notes

  • Scope: 99 files changed, 23,133 insertions, and 587 deletions versus main; the generated protobuf and TypeScript client files plus compiled browser assets are intentionally checked in.
  • The browser is an operator client, not an independent execution authority. It consumes ordered operator updates and requests snapshots/details on demand.
  • The protocol deliberately avoids transporting live Workflow objects across process or browser boundaries. Retained run topology is immutable and schema-only; execution configuration and declaration instructions remain in the current catalog.
  • Large-run behavior is intentionally bounded: browser state is ephemeral, details are paged/cancellable, and caches/queues have explicit limits rather than mirroring complete lifecycle history.
  • Review current-workflow versus run-view semantics closely: a run must continue to render its recorded topology after catalog reload, removal, or topology change.
  • Run the failed Python suite above before merge; its failures are documented here rather than represented as passing verification.

@glesperance glesperance left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bottom line: REQUEST CHANGES

Reviewed at HEAD 6f8d489 against origin/main (merge-base 97cd8a3). Controller gate in a clean worktree: lint PASS, tests 13 failed / 1181 passed (uv run pytest -m "not tmux and not ray") — the branch is red against its own suite, all failures inside the changed surface. Two independent read-only review lanes (correctness, quality-security) both returned REQUEST CHANGES; each finding below was verified against source before inclusion.

Findings

1. blocker: branch fails its own test suite (13 failures, all in the changed surface)

test/operator_tests/test_operator_dev_reload.py (10), test_state_detail.py (2), test_web.py (1).

  • test_prepare_failure_does_not_publish_run and the test_malformed_preparation_event_rolls_back_start matrix expect start_run to raise preparation failures synchronously; with the new async REQUESTING lifecycle it does not raise. Either the tests encode a contract this PR silently broke, or they were not migrated to the new contract — resolve explicitly, not by deletion.
  • test_close_owns_and_terminates_run_during_preparation expects an error to surface from start_run when the operator closes mid-preparation; none does (see finding 2).
  • test_browser_listener_serves_packaged_operator_application fails because the vendored src/runtime/operator/web_assets/index.html does not match the test's expectation — the packaged asset is stale relative to web/operator (see finding 6: nothing in CI regenerates or checks it).

2. major: close() races the async preparation thread

src/runtime/operator/operator.py:1240 installs and starts the preparation thread outside the operator lock, after REQUESTING publication. close() (operator.py:1483) joins handle.preparation_thread only when it is non-None, so it can complete shutdown before start_run launches preparation. The failure path (operator.py:1347-1354) then publishes into a closed operator (rejected at operator.py:2411), skips resource cleanup, and lets start_run return after closure. Synchronize preparation-thread ownership with close(), including a final locked _closed check before starting the thread.

3. major: trace re-materialization contaminates across invocations

src/runtime/operator/operator.py:2891-2912 rebuilds trace.steps and evidence.events from all retained events for the node, while the envelope's invocation_id (operator.py:2915) identifies only the latest trace invocation. A node that invokes an agent more than once produces a latest trace contaminated with earlier invocations' iterations, disagreeing with its own header counts. Filter reconstructed content by the finalized trace's invocation ID and add a two-invocation regression with distinct iteration.recorded steps.

4. major: catalog revision compared without operator epoch

src/runtime/operator/client.py:1587_install_catalog_locked discards any catalog with revision < self._catalog.revision without checking operator_instance_id. After an operator restart (revisions restart low), the fresh catalog is dropped during reset (client.py:1541) and later CatalogReplaced updates (client.py:1830), while the callback republishes the retained old-epoch catalog (client.py:1787-1793), which the TUI accepts by revision (src/tui/ui_store.py:2380). Compare revisions only within one epoch; always replace on epoch change.

5. major: gRPC-Web error semantics + unbounded browser hydration

  • src/runtime/operator/web.py:121: request decoding catches only ValueError, but ParseFromString (web.py:316) raises google.protobuf.message.DecodeError, which is not a ValueError subclass (verified). Malformed protobuf therefore yields an INTERNAL trailer instead of INVALID_ARGUMENT. Additionally, iterator failures after chunked headers are sent (web.py:179) escape to the outer handler, which attempts a second HTTP response (web.py:200) instead of translating inside _send_stream. Add malformed-payload and failing-generator regressions.
  • web/operator/src/api.ts:103: loadBaseline follows every unique continuation token and appends every summary with no page, item, or byte ceiling — a long-retained or faulty operator consumes unbounded browser memory before baseline publication. Add an explicit hydration budget and fail before partial publication.

6. major: incomplete V2 cutover + frontend gates missing from CI

  • The V1 flow_name selector remains beside workflow_selector (proto/operator.proto:31,198); server.py:62 accepts either and client.py:1240 keeps a legacy-name cache dual-populating requests. Project policy is clean V2-only cutovers — remove the legacy selector/fallback/cache. ARCHITECTURE.md:419,432 still documents ListFlows and StreamRunUpdates.
  • .github/workflows/ci.yml never invokes the new web-proto / web-build / web-test / web-bench Makefile targets, so TypeScript generation, compilation, tests, and vendored-asset sync can all regress while CI stays green (finding 1's stale web_assets is the first instance). Add the frontend gates plus a clean-diff check after generation/build.
  • minor web.py:43: _RPC_METHODS duplicates the generated service descriptor; a future proto RPC will work over native gRPC but 404 over gRPC-Web. Derive routing from the descriptor or add an exact parity test.

Test gaps

  • test_web.py lacks oversize-body, path-traversal, and disconnect cases (assert >4 MiB body → error trailer; encoded .. → 404; disconnected update stream unsubscribes).
  • No deterministic REQUESTING-cancel test: block preparation, cancel while REQUESTING, release, assert CANCELLED with no node execution and handle cleanup.

Downstream (Delta) impact is tracked in Trampoline-AI/trampoline-ai.delta#12 — the renames break Delta's facade imports on its next repin; no action needed in this PR beyond keeping the cutover clean per finding 6.

@magix022

magix022 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Translation:

  1. Shutdown can miss a run that is still starting.
    A run starts in the background. If you shut the operator down at exactly that moment, shutdown may not know the background
    startup exists yet. That background work then continues after shutdown and tries to use a closed operator.
    Fix:make startup and shutdown coordinate so shutdown always waits for—or cancels—the startup work.
  2. A node can show logs from the wrong attempt.
    If the same agent/node runs twice, opening the details for its second run can include steps and evidence from its first run.
    Fix:show only data belonging to the specific run attempt the user selected.
  3. Restarting the operator can leave the UI showing old workflows.
    The UI uses a revision number to decide whether workflow data is newer. After an operator restart, revisions restart from a
    small number, so the UI can reject the fresh data and keep data from the previous operator process.
    Fix:when the operator itself changes, discard the old catalog regardless of revision number.
  4. The web interface handles bad input and huge histories badly.
  • Sending corrupt protocol data returns “server crashed” instead of “your request is invalid.”
  • If a live stream fails halfway through, the server tries to send a second response after it already started the first.
  • When loading run history, the browser keeps fetching every page indefinitely. A large run history could consume all browser
    memory.
    Fix:return the correct client-error response, handle stream errors in-place, and cap how much historical data the browser
    loads.
  1. The migration is incomplete and CI does not protect the new frontend.
    The PR introduces a new workflow-selection API but leaves the obsolete API path in place. Documentation still names old APIs.
    CI also does not run the web build/tests, so broken frontend code or stale packaged web assets can merge unnoticed.
    Fix:delete the old API path, update docs, and make CI build/test/check generated frontend assets.

@magix022
magix022 force-pushed the add-operator-web-ui-and-hot-reload branch 4 times, most recently from 49f4b00 to 43bf408 Compare August 5, 2026 03:02
@magix022

magix022 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Requested changes addressed

The review was performed at 6f8d489; the current head is 43bf408. Here is how each finding was resolved.

1. Branch test failures / lifecycle contract

The new asynchronous lifecycle is now explicit: start_run() publishes a REQUESTING run immediately, while preparation/build/protocol failures terminalize that retained run as FAILED with an operator log rather than escaping synchronously from start_run(). The affected preparation, reload, state-detail, and packaged-asset assertions were updated to test that observable contract. The current head is green across Lint + tests, Browser UI, Ray tests, and Tmux TUI tests.

2. close() racing asynchronous preparation

Preparation-thread ownership is installed while holding the operator lock, before the thread can run. Startup rechecks the closed state at the synchronization boundary, and shutdown cancels/drains active work and joins the owned preparation thread. A close that overlaps REQUESTING can therefore no longer miss a not-yet-published thread or allow background preparation to publish into a closed operator.

3. Trace contamination across agent invocations

Finalized trace reconstruction is now scoped by invocation_id. Steps and evidence for a retained node trace are selected only from the invocation represented by that trace header, so a later invocation cannot inherit iterations or evidence from an earlier invocation of the same node. The regression coverage uses distinct invocations and verifies the reconstructed content/counts stay aligned.

4. Catalog revisions across operator restarts

Catalog monotonicity is now evaluated within an operator_instance_id epoch. A new operator instance always replaces the previous catalog regardless of its lower revision, while stale revisions are still rejected within the same epoch. Reset/baseline installation and catalog callbacks now carry that epoch consistently, preventing old-process workflows from surviving a restart.

5. gRPC-Web errors and bounded browser hydration

  • Malformed protobuf payloads, including DecodeError, are translated to INVALID_ARGUMENT instead of an internal server error.
  • Streaming failures are translated inside the already-started chunked response and emitted as gRPC-Web trailers; the handler no longer attempts a second HTTP response. Response iterators are closed during stream teardown/disconnect handling.
  • Browser baseline loading is bounded before publication: at most 100 pages, 10,000 run summaries, and 8 MiB of encoded summaries. Repeated/non-advancing page tokens fail immediately, and no partial baseline is installed.
  • gRPC-Web routing is derived from the generated service descriptor rather than a hand-maintained RPC list.

6. V2 cutover and frontend CI protection

  • The legacy flow_name request field, dual-population fallback, and client-side legacy-name cache were removed. workflow_selector is now the single run/list selector, with protobuf bindings, callers, tests, and architecture documentation updated together.
  • CI now regenerates the TypeScript client, runs all browser tests, builds the packaged assets, executes the volume and real-Chrome benchmark, and checks generated/protobuf/asset trees for a clean diff.
  • The real-browser job uses browser-actions/setup-chrome@v2. Its remaining failure was traced to the benchmark fixture importing the removed /src/tailwind.css; it now imports /src/style.css. The same Chrome-for-Testing 151 binary passed locally with 10,000 runs, 14 DOM rows, 102.90 ms render, and 41.50 ms interaction, and the current Browser UI Actions job is green.

Owner-requested cleanup

The interim CI fixes were rewritten into one commit, 0d16698 (ci: run browser benchmark with Chrome v2). The OpenSpec planning directory was removed in 43bf408 and is no longer present in the PR tree.

Current verification

@magix022
magix022 force-pushed the add-operator-web-ui-and-hot-reload branch from 43bf408 to d8a052e Compare August 5, 2026 03:17
@magix022
magix022 force-pushed the add-operator-web-ui-and-hot-reload branch from d8a052e to 6ce454d Compare August 5, 2026 03:34
@magix022
magix022 merged commit 8fc4594 into main Aug 5, 2026
4 checks passed
@magix022
magix022 deleted the add-operator-web-ui-and-hot-reload branch August 5, 2026 16:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants